package edu.kit.csl.cratyli.ui;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;


/**
 * This singleton class stores the configuration, which is normally
 * set by command line arguments or configuration file.
 * 
 * @see #processCommandLineArguments(String[])
 */
public class Configuration {
	
	/*
	 * File name of the default configuration file
	 */
	final static private String DEFAULT_CONFIGURATION_FILE = "cratyli.conf";
	
	/*
	 * Singleton instance
	 */
	static private Configuration conf;
	
	/*
	 * Parameter lists by their datatypes
	 */
	private Map<String, Integer> intParams;
	private Map<String, Float> floatParams;
	private Map<String, String> stringParams;
	private Map<String, String> descriptions;
	
	/*
	 * List of already included configuration files 
	 */
	private Set<String> alreadyIncluded;
	
	/*
	 * Add an integer parameter.
	 */
	private void addParameter(String key, int defaultValue, String desc) {
		intParams.put(key, defaultValue);
		descriptions.put(key, desc);
	}
	
	/*
	 * Add a floating point parameter.
	 */
	private void addParameter(String key, float defaultValue, String desc) {
		floatParams.put(key, defaultValue);
		descriptions.put(key, desc);
	}
	
	/*
	 * Add a string parameter.
	 */
	private void addParameter(String key, String defaultValue, String desc) {
		stringParams.put(key, defaultValue);
		descriptions.put(key, desc);
	}
	
	/*
	 * Private constructor.
	 */
	private Configuration() {
		descriptions = new HashMap<String, String>();
		intParams = new HashMap<String, Integer>();
		floatParams = new HashMap<String, Float>();
		stringParams = new HashMap<String, String>();
		alreadyIncluded = new HashSet<String>();
	}
	
	/**
	 * Restores the default configuration.
	 */
	public void restore() {
		descriptions.clear();
		intParams.clear();
		floatParams.clear();
		stringParams.clear();
		SimpleDateFormat formatter = new SimpleDateFormat("yy-MM-dd.HHmmss.");
		addParameter("configFile", "",
				"Configuration file (GIZA++ format: <key> <val>). Parameters "
				+ "in this file override values in the default configuration "
				+ "file. Command line arguments override all other settings. "
				+ "If this parameter is used within a config file, it has "
				+ "'include once' semantics.");
		addParameter("logLevel", "INFO",
				"Controls the amount of output. "
				+ "FATAL: Only fatal errors, "
				+ "ERROR: All errors, "
				+ "WARN: Warnings and errors, "
				+ "INFO: Notices, warnings and errors, "
				+ "DEBUG: Debug mode");
		addParameter( // Like GIZA++
				"outputPrefix",
				formatter.format(new Date()) + System.getProperty("user.name"),
				"Prefix which is prepended to all output files");
		addParameter("dumpFrequency", 1,
				"Dump frequency. Set to 1 to dump after each iteration.");
		addParameter("writingSystem", "p2g",
				"The writing system defines the way the phonemes in the target"
				+ " language are translated to a target language word. Only "
				+ "required when the SRILM merger is used. Possible values:\n"
				+ "'p2g': Use a mapping file p2gFile for the mapping between "
				+ "target language phonemes and graphemes.\n"
				+ "'dropBlanks': The target words are represented by the "
				+ "original phonemes without blanks.");
		addParameter("p2gFile", "p2g.conf",
				"This parameter is only significant if the writing system "
				+ "'p2g' is used. The file format is the same as in GIZA++");
		addParameter("primaryCratylusPath", "cratylus",
				"This parameter must be equal to the outputPrefix parameter "
				+ "of the primary cratylus run. The files *.dict, *.plain.trgt"
				+ ", and *.plain.src must be available. Optional, the *.giza "
				+ "file is translated as well. Additionally to the standard "
				+ "cratylus output files, a *.ids file is required containing "
				+ "IDs for each of the sentences in *.plain.* in order to map "
				+ "them to secondary cratylus runs.");
		addParameter("secondaryCratylusPaths", "cratylus",
				"Specify a number of paths to cratylus runs. The same set of "
				+ "files as for the primaryCratylusPath parameter must be "
				+ "available. Blank separate different secondary runs. The "
				+ "runs are incorporated to the primary run in the specified "
				+ "order.");
		addParameter("combinationOrder", "incremental",
				"Specifies the order in which the primary and secondary "
				+ "cratylus runs are combined. Possible values are:\n"
				+ "incremental: Successively add the secondary runs to the "
					+ "primary cratylus run.\n"
				+ "tree: Hierarchical bottom-up combination.");
		addParameter("m2mAlignerCommand", "./m2m-aligner --maxX 3 --maxY 3",
				"Path to the m2m aligner binary (available at "
				+ "http://m2m-aligner.googlecode.com) including parameters. "
				+ "The parameter -i (input file) and -o (output file) and "
				+ "-alignOut (model output file) are added automatically.");
		addParameter("m2mLibraryPath", ".",
				"Path to the m2m libraries (LD_LIBRARY_PATH).");
		addParameter("sentenceTargetFunction", "mostGranular",
				"Optimization criterium for the combination strategy. Possible"
				+ " values are:\n"
				+ "mostGranular: Maximize the sentence length\n"
				+ "weightSum: Maximize the sum of word weights\n"
				+ "weightMin: Maximize the minimum of word weights");
		addParameter("merger", "srilm",
				"The merger combines the pronunciations of words with 1:1 "
				+ "alignments. Possible values are:\n"
				+ "max: Pick the pronunciation with the heighest weight.\n"
				+ "srilm: Run nbest-lattice from the SRI language modeling "
					+ "toolkit. Note the mergerToolPath and "
					+ "mergerToolArgs parameter.");
		addParameter("mergerToolPath", "",
				"If the merger requires a tool, this parameter has to point"
				+ " to a executable binary of it.");
		addParameter("mergerToolArgs", "",
				"This parameter contains command line arguments passed through"
				+ " to the merger tool (if used).");
		addParameter("m2mCutOffProbability", 0.1f,
				"Probabilities in the m2m alignment model beyond this "
				+ "threshold are ignored (i.e. during merging)");
		addParameter("secondaryWeightLimit", 3.0f,
				"If the weight of a secondary word is more than this factor "
				+ "larger than the primary weight, do not use it in the "
				+ "pronunciation merging process.");
		addParameter("minimumWordWeight", 0.01f,
				"All words with a weight lower than this threshold are removed"
				+ " from the corpus before each combination iteration.");
		addParameter("annealingFunction", "constant",
				"The annealing function controls the weight between primary "
				+ "and secondary words when they are combined by the merger."
				+ " This ratio might change over the number of secondary "
				+ "cratylus runs processed so far. Possible values are:\n"
				+ "constant: Set the ratio fixed to startTemperature.\n"
				+ "root: sqrt(x) translated and scaled such that it "
					+ "matches startTemperature and endTemperature");
		addParameter("startTemperature", 1.0f,
				"The start temperature of the annealing function.");
		addParameter("endTemperature", 1.0f,
				"The end tempterature of the annealing function (should be "
				+ "greater than startTemperature.");
	}
	
	/**
	 * Get configuration singleton. If it does not exist yet, create it and 
	 * load default configuration file.
	 * 
	 * @return configuration singleton
	 */
	static public Configuration getSingleton() {
		if (conf == null) {
			conf = new Configuration();
			conf.restore();
			conf.loadConfigurationFile(DEFAULT_CONFIGURATION_FILE);
		}
		return conf;
	}
	
	/**
	 * Set the value of a parameter.
	 * 
	 * @param key
	 * @param value
	 */
	public void set(String key, int value) {
		if (!intParams.containsKey(key)) {
			Logger.getSingleton().fatalError(
					"Unknown integer parameter " + key);
		} else {
			intParams.put(key, value);
		}
	}
	
	/**
	 * Set the value of a parameter.
	 * 
	 * @param key
	 * @param value
	 */
	public void set(String key, float value) {
		if (!floatParams.containsKey(key)) {
			Logger.getSingleton().fatalError(
					"Unknown floating point parameter " + key);
		} else {
			floatParams.put(key, value);
		}
	}
	
	/**
	 * Get a integer parameter. If the key is unknown, return null
	 * 
	 * @param key the name of the parameter
	 * @return the value of the parameter or null
	 */
	public Integer getInteger(String key) {
		return intParams.get(key);
	}
	
	/**
	 * Get a floating point parameter. If the key is unknown, return null
	 * 
	 * @param key the name of the parameter
	 * @return the value of the parameter or null
	 */
	public Float getFloat(String key) {
		return floatParams.get(key);
	}
	
	/**
	 * Get a string parameter. If the key is unknown, return null
	 * 
	 * @param key the name of the parameter
	 * @return the value of the parameter or null
	 */
	public String getString(String key) {
		return stringParams.get(key);
	}
	
	/**
	 * Set the value of a parameter.
	 * 
	 * @param key
	 * @param value
	 */
	public void set(String key, String value) {
		try {
			if (intParams.containsKey(key)) {
				set(key, Integer.parseInt(value));
			} else  if (floatParams.containsKey(key)) {
				set(key, new Float(value));
			} else if (stringParams.containsKey(key)) {
				stringParams.put(key, value);
				if (key.equals("logLevel")) {
					Logger.getSingleton().setLevel(value);
				}
			} else {
				Logger.getSingleton().fatalError(
						"Unknown parameter " + key);
			}
		} catch(NumberFormatException e) {
			Logger.getSingleton().fatalError(
					"Parameter " + key + " should be numeric");
		}
	}
	
	/**
	 * Parse the command line arguments and overrides parameters if they
	 * appear in them.
	 * 
	 * @param args string array with command line arguments
	 */
	public void processCommandLineArguments(String[] args) {
		String curParam = null;
		for (String arg : args) {
			if (arg.charAt(0) == '-') {
				if (curParam != null) {
					set(curParam, 1);
				}
				curParam = arg.substring(1);
			} else if (curParam != null) {
				set(curParam, arg);
				if (curParam.equals("configFile") 
						&& loadConfigurationFile(arg)) {
					// Process command line arguments again
					processCommandLineArguments(args);
					return;
				}
				curParam = null;
			} else {
				Logger.getSingleton().fatalError(
						"Malformed command line argument " + arg);
			}
		}
	}
	
	/**
	 * Load parameters from a configuration file. The file should be in GIZA++
	 * format (i.e. one parameter per line, key/value separated by a blank).
	 * Lines starting with '#' are ignored.
	 * 
	 * @param fileName path to the file to load
	 * @return <code>true</code> if the file could be loaded, false otherwise
	 */
	public boolean loadConfigurationFile(String fileName) {
		if (alreadyIncluded.contains(fileName)) {
			return false;
		}
		Logger log = Logger.getSingleton();
		alreadyIncluded.add(fileName);
		try {
			FileInputStream fstream = new FileInputStream(fileName);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String strLine;
			// Read File Line By Line
			while ((strLine = br.readLine()) != null) {
				if (strLine.charAt(0) == '#') { // Comment
					continue;
				}
				String[] split = strLine.split(" ", 2);
				if (split.length != 2) {
					log.error("Malformed line " + strLine + " in " + fileName);
				}
				set(split[0], split[1]);
			}
			in.close();
		} catch (Exception e) {// Catch exception if any
			log.error("Error while reading config file "
					+ fileName + ": " + e.getMessage());
			return false;
		}
		return true;
	}
	
	/**
	 * Prints a help text to stdout.
	 */
	public void printHelp() {
		System.out.println("Cratylus is a tool for extracting training data"
				+ " for ASR and MT systems from cross-lingual word-to-"
				+ "phoneme alignments found by PISA.");
		System.out.println(
				"Configuration via command line arguments: -<name> <value>");
		System.out.println(
				"Configuration via file (per line): <name> <value>");
		System.out.println("Following parameters are available.");
		System.out.println("");
		System.out.println("String parameters");
		System.out.println("---------------------");
		for (Map.Entry<String, String> entry : stringParams.entrySet()) {
			printParameter(entry.getKey(), entry.getValue(),
					descriptions.get(entry.getKey()));
		}
		System.out.println("");
		System.out.println("Integer parameters");
		System.out.println("---------------------");
		for (Map.Entry<String, Integer> entry : intParams.entrySet()) {
			printParameter(entry.getKey(), entry.getValue().toString(),
					descriptions.get(entry.getKey()));
		}
		System.out.println("");
		System.out.println("Floating point parameters");
		System.out.println("---------------------");
		for (Map.Entry<String, Float> entry : floatParams.entrySet()) {
			printParameter(entry.getKey(), entry.getValue().toString(),
					descriptions.get(entry.getKey()));
		}
	}
	
	/*
	 * Prints a single parameter to stdout.
	 */
	private void printParameter(String key, String value, String description) {
		if (value.equals("")) {
			value = "<not set>";
		}
		System.out.println(key + " (Default: " + value + ")");
		System.out.println(description);
		System.out.println("");
	}
}
